﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;

namespace TNCGameEngine.Scripting
{

    public struct ReturnValue
    {
        public bool Valid;
        public object Object;
        public Type Type;

        public ReturnValue(bool valid, object obj, Type type)
        {
            this.Valid = valid;
            this.Object = obj;
            this.Type = type;
        }

    }

    public class Script
    {

        #region "Properties"

        private string file = string.Empty;
        public string File
        {
            get { return this.file; }
        }

        private bool loaded = false;
        public bool FileLoaded
        {
            get { return this.loaded; }
        }
                
        #endregion

        #region "Variables"

        private static ScriptManager manager = null;
        private static bool managerLoaded = false;
        
        internal List<string> Namepsaces = null;
        private Dictionary<string, ScriptFunction> Functions = null;
        
        private char[] separators_ = new char[] { '.', '(', '[', ',', ')', ';', '{', '}' };
        private char[] seps = new char[] { '.' };

        #endregion

        #region "Contructors"

        public Script(string file)
        {
            this.Namepsaces = new List<string>();
            this.file = this.CleanFilePath(file);
        }

        #endregion

        #region "Methods"

        public bool AddUsing(string str)
        {
            if (str.EndsWith(";")) { str = str.Substring(0, str.Length - 1); }
            if (str.Replace(".", string.Empty).Length > 1)
            {
                this.Namepsaces.Add(!str.EndsWith(".") ? str + "." : str);
                return true;
            }
            return false;
        }

        internal static Type GetType(string name, List<string> namespaces)
        {

            Type t = null;

            // already cached?
            if (ScriptManager.Types.TryGetValue(name, out t)) { return t; }
                       
            // check in the referenced assemblies
            foreach (string s in Script.manager.LinkedAssemblies)
            {
                if (Script.GetTypeFromAssembly(name, s, namespaces, out t)) 
                {
                    ScriptManager.Types.Add(name, t);
                    return t;
                }
            }

            throw new Exception(string.Format("The type '{0}' does not exist in any referenced assemblies", name));

        }

        internal static bool GetTypeFromAssembly(string name, string assembly, List<string> namespaces, out Type type)
        {
            type = Type.GetType(name + "," + assembly);
            if (type == null)
            {
                foreach (string s in namespaces)
                {
                    type = Type.GetType(s + name + "," + assembly);
                }
            }
            return (type != null);
        }

        private string CleanFilePath(string file)
        {
            file = file.Trim().ToLower();
            string[] strs = file.Split(this.seps);
            if (strs.LongLength > 1)
            {
                file = strs[0];
            }
            return file;
        }

        public void LoadFile()
        {
            this.LoadFile(this.file);
        }

        private void LoadFile(string file)
        {

            if (CheckAndThrowException()) { return; }

            string path = Script.manager.RootDirectory + "\\" + file + "." + Script.manager.FileExtension;

            if (!System.IO.File.Exists(path)) { throw new Exception(string.Format("Script file '{0}' missing.", path)); }

            List<List<string>> fs = ScriptFunction.GetFuncLines(System.IO.File.ReadAllLines(path), path, this.Namepsaces);
            this.Functions = new Dictionary<string, ScriptFunction>(fs.Count);
            ScriptFunction scfn = null;
            foreach (List<string> list in fs)
            {
                scfn = new ScriptFunction(this, list);
                if (this.Functions.ContainsKey(scfn.Name)) 
                { 
                    throw new Exception(string.Format("Syntax error; the duplicate function name '{0}'.", scfn.Name)); 
                }
                this.Functions.Add(scfn.Name, scfn);
            }
            this.loaded = true;

        }

        public ReturnValue EvaluateFunction(string name, params object[] args)
        {
            
            if (!this.loaded) { throw new Exception("The script has not yet been loaded."); }

            name = name.Trim();
            ScriptFunction f = null;
            if (this.Functions.TryGetValue(name, out f))
            {
                if (f.Valid)
                {
                    if (f.Args.Count <= args.Length)
                    {
                        f.InitializeArgs(args);
                        foreach (Cmd cmd in f.Commands)
                        {
                            if (f.Returned) { break; }
                            if (cmd.Execute) { cmd.Eval(f); }
                        }
                        object o;
                        if (f.GetReturnObject(out o))
                        {
                            return new ReturnValue(true, o, o.GetType());
                        }
                        else
                        {
                            return new ReturnValue(false, null, null);
                        }
                    }
                    else
                    {
                        throw new Exception(string.Format("Mismatch in the number of arguments passed into '{0}'.", name));
                    }
                }
                else
                {
                    throw new Exception(string.Format("Syntax error in function '{0}', please check the script.", name));
                }
            }
            else
            {
                throw new Exception(string.Format("The function '{0}' does not exist in the loaded script.", name));
            }

        }

        internal static void InitializeScripting(ScriptManager manager)
        {            
            Script.manager = manager;
            Script.managerLoaded = (manager != null);
        }

        private static bool CheckAndThrowException()
        {
            if (!Script.managerLoaded)
            {
                throw new Exception("Scripting manager needs to be initialized using ScriptingManager.Initialize().");
            }
            return false;
        }








        internal object Eval(string expr, IProperty globals)
        {

            int offset = 0;
            object ret = this.Eval(expr, ref offset, null, globals);
            if (offset != expr.Length)
            {
                throw new Exception(String.Format("Warning: Script.Eval() returned before parsing entire expression: {0}", expr));
            }
            else if (ret == null)
            {
                throw new Exception(String.Format("Warning: Script.Eval() returned null: {0}", expr));
            }
            return ret;
        }

        internal object Eval(string expr, ref int offset, object current, IProperty globals)
        {

            if (Script.CheckAndThrowException()) { return null; }

            if (current == null) { current = globals; }

            //  re-use the start if using semicolons
            while (offset < expr.Length)
            {

                int end = expr.IndexOfAny(separators_, offset);
                if (end < 0) { end = expr.Length; }
                string cur = expr.Substring(offset, end - offset).Trim();
                char sep = (end == expr.Length) ? (char)0 : expr[end];
                offset = end + ((sep == 0) ? 0 : 1);

                switch (sep)
                {
                    case (char)0:
                    case '.':
                    case ',':
                    case ')':
                    case ';':
                    case '}':
                        //  literals allowed when reading function args or assignments
                        current = this.EvalProp(globals, current, cur, (sep == ',') || (sep == ')') || (sep == '}'));
                        break;
                    case '=':
                        current = this.EvalAssignment(globals, current, cur, expr, ref offset);
                        break;
                    case '{':
                        current = this.EvalProp(globals, current, cur, false);
                        current = this.EvalLockedContext(globals, current, cur, expr, ref offset);
                        break;
                    case '(':
                        current = this.EvalFunction(globals, current, cur, expr, ref offset);
                        break;
                    case '[':
                        current = this.EvalIndex(globals, current, cur, expr, ref offset);
                        break;
                    default:
                        System.Diagnostics.Debug.Fail(String.Format("Impossible case in Script.Eval(): separator {0} at offset {1}", sep, offset));
                        break;
                }

                //  if I want to run multiple "things" then I start over at some point
                if (sep == ';' || (offset <= expr.Length && expr[offset - 1] == ';'))
                {
                    current = globals;
                }

                //  if I run out of data, stop searching
                if (current == null)
                {
                    if (offset < expr.Length)
                    {
                        throw new Exception(String.Format("Eval(): got null return at {0} before {1}", cur, expr.Substring(offset)));
                    }
                    return null;
                }

                //  break out if this is the end of an argument
                if (expr[offset - 1] == ',' || expr[offset - 1] == ')' || expr[offset - 1] == '}')
                {
                    break;
                }

                while (offset < expr.Length && (expr[offset] == ' ' || expr[offset] == (char)10 || expr[offset] == (char)13 || expr[offset] == (char)8))
                {
                    ++offset;
                }

            }

            return current;

        }

        internal object EvalProp(IProperty globals, object current, string prop, bool allowLiteral)
        {

            IProperty cr = current as IProperty;

            if (cr != null)
            {
                object ret;
                if (cr.GetPropertyValue(prop, out ret)) { return ret; }
            }

            System.Reflection.PropertyInfo pi = current.GetType().GetProperty(prop, BindingFlags.Instance
                                                                                | BindingFlags.Public
                                                                                | BindingFlags.NonPublic
                                                                                | BindingFlags.Static
                                                                                | BindingFlags.FlattenHierarchy);

            if (pi == null)
            {
                if (allowLiteral)
                {
                    if (prop.Length > 1 && prop[0] == '"' && prop[prop.Length - 1] == '"')
                    {
                        prop = prop.Substring(1, prop.Length - 2);
                    }
                    return prop;
                }
                throw new ArgumentException(String.Format("Script.Eval(): Type {0} doesn't have property {1}.", current.GetType().Name, prop));
            }

            if (!pi.CanRead)
            {
                throw new ArgumentException(String.Format("Script.Eval(): Type {0} property {1} is write-only.", current.GetType().Name, prop));
            }

            return pi.GetValue(current, null);

        }
        
        //  get a string listing properties of a given object, matching the given pattern (StartsWith)
        internal string GetProps(object o, string pat = "")
        {
            String ret = o.GetType().Name + " Props: ";
            PropertyInfo[] properties = o.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
            foreach (PropertyInfo pi in properties)
            {
                if (pat.Length == 0 || pi.Name.StartsWith(pat))
                {
                    ret = ret + pi.Name + ", ";
                }
            }
            return ret;
        }

        //  get a string listing methods of a given object, matching the given pattern (StartsWith)
        internal string GetMethods(object o, string pat = "")
        {
            String ret = o.GetType().Name + " Methods: ";
            MethodInfo[] methods = o.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
            foreach (MethodInfo mi in methods)
            {
                if (pat.Length == 0 || mi.Name.StartsWith(pat))
                {
                    ret = ret + mi.Name + ", ";
                }
            }
            return ret;
        }

        internal object EvalFunction(IProperty globals, object current, string func, string expr, ref int offset)
        {

            //  first, find matching methods by name
            MethodInfo[] methods = current.GetType().GetMethods();
            List<MethodInfo> found = new List<MethodInfo>();
            foreach (MethodInfo mi in methods)
            {
                if (mi.Name == func) { found.Add(mi); }
            }

            //  second, parse all arguments
            object[] args = this.EvalArgs(globals, expr, ref offset);
            object[] matched = new object[args.Length];

            //  third, try to match arguments to found methods
            object ret = null;
            foreach (MethodInfo mi in found)
            {
                if (this.MatchArgs(args, mi, matched))
                {
                    ret = mi.Invoke(current, matched);
                    while (offset < expr.Length)
                    {
                        if (expr[offset] == ' ' || expr[offset] == (char)10 || expr[offset] == (char)13 || expr[offset] == (char)8)
                        {
                            ++offset;
                            continue;
                        }
                        if (expr[offset] != '.' && expr[offset] != ';')
                        {
                            throw new ArgumentException(String.Format("Function call {0} can only be followed by a property dereference (.) or terminator (;).", func));
                        }
                        ++offset;
                        break;
                    }
                    return ret;
                }
            }

            throw new ArgumentException(String.Format("Type {0} doesn't have method {1} with {2} arguments.", current.GetType().Name, func, args.Length));

        }

        internal object EvalAssignment(IProperty globals, object current, string prop, string expr, ref int offset)
        {
            if (current == null)
                throw new ArgumentException(String.Format("Path.Eval(): Assignment of {0} to NULL object.", prop));
            IProperty ass = current as IProperty;
            if (ass != null)
            {
                object val = Eval(expr, ref offset, current, globals);
                ass.SetPropertyValue(prop, val);
                return val;
            }
            System.Reflection.PropertyInfo pi = current.GetType().GetProperty(prop, BindingFlags.Instance
                | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
            if (pi == null)
                throw new ArgumentException(String.Format("Path.Eval(): Type {0} doesn't have property {1}.", current.GetType().Name, prop));
            if (!pi.CanWrite)
                throw new ArgumentException(String.Format("Path.Eval(): Type {0} property {1} is read-only.", current.GetType().Name, prop));
            object opt = Eval(expr, ref offset, current, globals);
            if (pi.PropertyType.IsEnum && opt is string)
                opt = Enum.Parse(pi.PropertyType, (string)opt, false);
            pi.SetValue(current, opt, null);
            return opt;
        }

        internal object EvalLockedContext(IProperty globals, object current, string prop, string expr, ref int offset)
        {
            if (expr[offset] == '}')
            {
                ++offset;
                return current;
            }
            while (offset < expr.Length && expr[offset - 1] != '}')
            {
                //  each sub-expression is evaluated in the context of the "current"
                object eval = Eval(expr, ref offset, current, globals);
                if (expr[offset - 1] != '}' && expr[offset - 1] != ',')
                    throw new System.FormatException(String.Format("Syntax error in expression at offset {0}: {1} unexpected.",
                        offset, expr[offset - 1]));
            }
            return current;
        }

        //  offset points at the first argument; eval successive args separated by ',' until getting to ')'
        internal object[] EvalArgs(IProperty globals, string expr, ref int offset)
        {
            if (expr[offset] == ')')
            {
                ++offset;
                return new object[0];
            }
            List<object> args = new List<object>();
            while (offset < expr.Length && expr[offset - 1] != ')')
            {
                object eval = Eval(expr, ref offset, null, globals);
                if (expr[offset - 1] != ')' && expr[offset - 1] != ',')
                    throw new System.FormatException(String.Format("Syntax error in expression at offset {0}: {1} unexpected.",
                        offset, expr[offset - 1]));
                args.Add(eval);
            }
            return args.ToArray();
        }

        class DummyFormatProvider : IFormatProvider
        {
            public object GetFormat(Type formatType)
            {
                return null;
            }
        }
        IFormatProvider formatProvider = new DummyFormatProvider();

        //  Attempt to match up the given arguments with the arguments required 
        //  by this particular MethodInfo, for overload resolution.
        internal bool MatchArgs(object[] args, MethodInfo mi, object[] matched)
        {
            ParameterInfo[] pi = mi.GetParameters();
            if (pi.Length != args.Length)
                return false;
            for (int i = 0; i < pi.Length && i < args.Length; ++i)
            {
                try
                {
                    //  try Convert, which only works on base data types
                    if (pi[i].ParameterType.Equals(typeof(object)) || args[i].GetType().Equals(pi[i].ParameterType))
                        matched[i] = args[i];
                    else
                        matched[i] = Convert.ChangeType(args[i], pi[i].ParameterType, formatProvider);
                }
                catch (System.Exception x)
                {
                    GC.KeepAlive(x);
                    //  Convert couldn't do it, so try using TryParse() and TypeConverter.
                    try
                    {
                        matched[i] = ParseValue(pi[i].ParameterType, (string)args[i]);
                    }
                    catch (System.Exception y)
                    {
                        //  could not convert
                        throw new Exception(String.Format("Eval MatchArgs exception: {0}", y.Message));                        
                    }
                }
            }
            return true;
        }

        internal object EvalIndex(IProperty globals, object current, string index, string expr, ref int offset)
        {
            throw new NotImplementedException(String.Format("Indexing is not yet implemented (type {0}, index {1}, args{2})",
                                                    current.GetType(), index, expr.Substring(offset)));
        }

        #endregion

        object[] args = new object[2] { null, null };

        public object ParseValue(Type t, string str)
        {
            //  If there is a "TryParse" function on the object, try using that.
            args[0] = str;
            MethodInfo m = t.GetMethod("TryParse", BindingFlags.Static | BindingFlags.InvokeMethod);
            if (m != null)
            {
                if ((Boolean)m.Invoke(null, args))
                {
                    return args[1];
                }
            }
            //  If not found, use a type converter. This will fail if none installed.
            TypeConverter tc = TypeDescriptor.GetConverter(t);
            if (tc == null)
                throw new NotSupportedException(String.Format("No type converter found for {0}.", t.Name)); ;
            return tc.ConvertFromInvariantString(str);
        }

    }

}
